package com.comphenix.protocol.injector;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;

import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginLoadOrder;

import com.google.common.collect.Sets;

/**
 * Determine if a plugin using ProtocolLib is correct.
 * 
 * @author Kristian
 */
class PluginVerifier {
	/**
	 * A named plugin cannot be found.
	 * @author Kristian
	 */
	public static class PluginNotFoundException extends RuntimeException {
		/**
		 * Generated by Eclipse.
		 */
		private static final long serialVersionUID = 8956699101336877611L;

		public PluginNotFoundException() {
			super();
		}

		public PluginNotFoundException(String message) {
			super(message);
		}
	}
	
	public enum VerificationResult {
		VALID,
		
		/**
		 * The plugin doesn't depend on ProtocolLib directly or indirectly.
		 */
		NO_DEPEND;
		
		/**
		 * Determine if the verification was valid.
		 */
		public boolean isValid() {
			return this == VALID;
		}
	}
	
	/**
	 * Contains a list of plugins that will detect and use ProtocolLib dynamically, instead of relying on the dependency system.
	 */
	private static final Set<String> DYNAMIC_DEPENDENCY = Sets.newHashSet("mcore", "MassiveCore");
	
	/**
	 * Set of plugins that have been loaded after ProtocolLib.
	 */
	private final Set<String> loadedAfter = new HashSet<String>();
	
	/**
	 * Reference to ProtocolLib.
	 */
	private final Plugin dependency;
	
	/**
	 * Construct a new plugin verifier.
	 * @param dependency - reference to ProtocolLib, a dependency we require of plugins.
	 */
	public PluginVerifier(Plugin dependency) {
		if (dependency == null)
			throw new IllegalArgumentException("dependency cannot be NULL.");

		try {
			// This would screw with the assumption in hasDependency(Plugin, Plugin)
			if (safeConversion(dependency.getDescription().getLoadBefore()).size() > 0)
				throw new IllegalArgumentException("dependency cannot have a load directives.");
		} catch (LinkageError e) {
			// They're probably using an ancient version of Bukkit
			dependency.getLogger().log(Level.WARNING, "Failed to determine loadBefore: " + e);
		}

		this.dependency = dependency;
	}

	/**
	 * Retrieve a plugin by name.
	 * @param pluginName - the non-null name of the plugin to retrieve.
	 * @return The retrieved plugin.
	 * @throws PluginNotFoundException If a plugin with the given name cannot be found.
	 */
	private Plugin getPlugin(String pluginName) {
		Plugin plugin = getPluginOrDefault(pluginName);
		
		// Ensure that the plugin exists
		if (plugin != null)
			return plugin;
		else
			throw new PluginNotFoundException("Cannot find plugin " + pluginName);
	}
	
	/**
	 * Retrieve a plugin by name.
	 * @param pluginName - the non-null name of the plugin to retrieve.
	 * @return The retrieved plugin, or NULL if not found.
	 */
	private Plugin getPluginOrDefault(String pluginName) {
		return dependency.getServer().getPluginManager().getPlugin(pluginName);
	}
	
	/**
	 * Performs simple verifications on the given plugin.
	 * <p>
	 * Results may be cached.
	 * @param pluginName - the plugin to verify.
	 * @return A verification result.
	 * @throws IllegalArgumentException If plugin name is NULL.
	 * @throws PluginNotFoundException If a plugin with the given name cannot be found.
	 */
	public VerificationResult verify(String pluginName) {
		if (pluginName == null)
			throw new IllegalArgumentException("pluginName cannot be NULL.");
		return verify(getPlugin(pluginName));
	}
	
	/**
	 * Performs simple verifications on the given plugin.
	 * <p>
	 * Results may be cached.
	 * @param plugin - the plugin to verify.
	 * @return A verification result.
	 * @throws IllegalArgumentException If plugin name is NULL.
	 * @throws PluginNotFoundException If a plugin with the given name cannot be found.
	 */
	public VerificationResult verify(Plugin plugin) {
		if (plugin == null)
			throw new IllegalArgumentException("plugin cannot be NULL.");
		String name = plugin.getName();
		
		// Skip the load order check for ProtocolLib itself
		if (!dependency.equals(plugin)) {
			if (!loadedAfter.contains(name) && !DYNAMIC_DEPENDENCY.contains(name)) {
				if (verifyLoadOrder(dependency, plugin)) {
					// Memorize
					loadedAfter.add(plugin.getName());
				} else {
					return VerificationResult.NO_DEPEND;
				}
			}
		}
		
		// Everything seems to be in order
		return VerificationResult.VALID;
	}
	
	/**
	 * Determine if a given plugin is guarenteed to be loaded before the other.
	 * <p>
	 * Note that the before plugin is assumed to have no "load" directives - that is, plugins to be
	 * loaded after itself. The after plugin may have "load" directives, but it is irrelevant for our purposes.
	 * @param beforePlugin - the plugin that is loaded first.
	 * @param afterPlugin - the plugin that is loaded last.
	 * @return TRUE if it will, FALSE if it may or must load in the opposite other.
	 */
	private boolean verifyLoadOrder(Plugin beforePlugin, Plugin afterPlugin) {
		// If a plugin has a dependency, it will be loaded after its dependency
		if (hasDependency(afterPlugin, beforePlugin)) {
			return true;
		}
		
		// No dependency - check the load order
		if (beforePlugin.getDescription().getLoad() == PluginLoadOrder.STARTUP &&
			afterPlugin.getDescription().getLoad() == PluginLoadOrder.POSTWORLD) {
			return true;
		}
		return false;
	}
	
	/**
	 * Determine if a plugin has a given dependency, either directly or indirectly.
	 * @param plugin - the plugin to check.
	 * @param dependency - the dependency to find.
	 * @return TRUE if the plugin has the given dependency, FALSE otherwise.
	 */
	private boolean hasDependency(Plugin plugin, Plugin dependency) {
		return hasDependency(plugin, dependency, Sets.<String>newHashSet());
	}
	
	/**
	 * Convert a list to a set.
	 * <p>
	 * A null list will be converted to an empty set.
	 * @param list - the list to convert.
	 * @return The converted list.
	 */
	private Set<String> safeConversion(List<String> list) {
		if (list == null)
			return Collections.emptySet();
		else
			return Sets.newHashSet(list);
	}
	
	// Avoid cycles. DFS.
	private boolean hasDependency(Plugin plugin, Plugin dependency, Set<String> checking) {
		Set<String> childNames = Sets.union(
				 safeConversion(plugin.getDescription().getDepend()),
				 safeConversion(plugin.getDescription().getSoftDepend())
		);
		
		// Ensure that the same plugin isn't processed twice
		if (!checking.add(plugin.getName())) {
			throw new IllegalStateException("Cycle detected in dependency graph: " + plugin);
		}
		// Look for the dependency in the immediate children
		if (childNames.contains(dependency.getName())) {
			return true;
		}
		
		// Recurse through their dependencies
		for (String childName : childNames) {
			Plugin childPlugin = getPluginOrDefault(childName);
			
			if (childPlugin != null && hasDependency(childPlugin, dependency, checking)) {
				return true;
			}
		}
		
		// Cross edges are permitted
		checking.remove(plugin.getName());
		
		// No dependency found!
		return false;
	}
}
